// -------------------------------------------------
// BlinKit - BlinKit Library
// -------------------------------------------------
//   File Name: qjs_document.cpp
// Description: Bindings for Document
//      Author: Ziming Li
//     Created: 2022-03-08
// -------------------------------------------------
// Copyright (C) 2022 MingYang Software Technology.
// -------------------------------------------------

#include "./qjs_bindings.h"

#include "blinkit/blink/renderer/bindings/core/qjs/qjs.h"
#include "blinkit/blink/renderer/bindings/core/qjs/qjs_c_function_list_entries.h"
#include "blinkit/blink/renderer/bindings/core/script_controller.h"
#include "blinkit/blink/renderer/core/css/style_sheet_list.h"
#include "blinkit/blink/renderer/core/dom/Comment.h"
#include "blinkit/blink/renderer/core/dom/DocumentFragment.h"
#include "blinkit/blink/renderer/core/dom/element.h"
#include "blinkit/blink/renderer/core/dom/Text.h"
#include "blinkit/blink/renderer/core/events/Event.h"

/**
 * https://developer.mozilla.org/zh-CN/docs/Web/API/Document
 */

namespace BlinKit {
namespace qjs {

namespace Strings {
static const char Body[] = "body";
static const char CreateComment[] = "createComment";
static const char CreateDocumentFragment[] = "createDocumentFragment";
static const char CreateElement[] = "createElement";
static const char CreateEvent[] = "createEvent";
static const char CreateTextNode[] = "createTextNode";
static const char DocumentElement[] = "documentElement";
static const char GetElementById[] = "getElementById";
static const char Title[] = "title";
}

static JSValue CreateComment(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
{
    if (!JS_IsString(argv[0]))
        return JS_ThrowTypeError(ctx, "Invalid data");

    Document *document = ScriptWrappable::Cast<Document>(thisVal);

    GCRefPtr<Comment> ret = document->createComment(ToWTFString(ctx, argv[0]));
    return ScriptController::From(ctx)->Wrap(ret.get());
}

static JSValue CreateDocumentFragment(JSContext *ctx, JSValueConst thisVal, int, JSValueConst *)
{
    Document *document = ScriptWrappable::Cast<Document>(thisVal);

    GCRefPtr<DocumentFragment> ret = document->createDocumentFragment();
    return ScriptController::From(ctx)->Wrap(ret.get());
}

static JSValue CreateElement(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
{
    if (!JS_IsString(argv[0]))
        return JS_ThrowTypeError(ctx, "Invalid tagName");

    Document *document = ScriptWrappable::Cast<Document>(thisVal);

    Context c(*ctx);
    GCRefPtr<Element> ret = document->createElement(ToAtomicString(ctx, argv[0]), c);
    if (c.hadException())
        return c.ReturnException();
    return ScriptController::From(ctx)->WrapElement(ret.get());
}

static JSValue CreateEvent(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
{
    if (!JS_IsString(argv[0]))
        return JS_ThrowTypeError(ctx, "Invalid type");

    Document *document = ScriptWrappable::Cast<Document>(thisVal);

    Context c(*ctx);
    GCRefPtr<Event> ret = document->createEvent(ToAtomicString(ctx, argv[0]), c);
    if (c.hadException())
        return c.ReturnException();

    return ScriptController::From(ctx)->WrapEvent(ret.get());
}

static JSValue CreateTextNode(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
{
    if (!JS_IsString(argv[0]))
        return JS_ThrowTypeError(ctx, "Invalid data");

    Document *document = ScriptWrappable::Cast<Document>(thisVal);

    GCRefPtr<Text> ret = document->createTextNode(ToWTFString(ctx, argv[0]));
    return ScriptController::From(ctx)->Wrap(ret.get());
}

static JSValue GetBody(JSContext *ctx, JSValueConst thisVal)
{
    Document *document = ScriptWrappable::Cast<Document>(thisVal);
    return ScriptController::From(ctx)->WrapElement(document->body());
}

static JSValue GetDocumentElement(JSContext *ctx, JSValueConst thisVal)
{
    Document *document = ScriptWrappable::Cast<Document>(thisVal);
    return ScriptController::From(ctx)->WrapElement(document->documentElement());
}

static JSValue GetElementById(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
{
    if (!JS_IsString(argv[0]))
        return JS_ThrowTypeError(ctx, "Invalid id");

    Document *document = ScriptWrappable::Cast<Document>(thisVal);

    Element *ret = document->getElementById(ToAtomicString(ctx, argv[0]));
    return ScriptController::From(ctx)->WrapElement(ret);
}

static JSValue GetTitle(JSContext *ctx, JSValueConst thisVal)
{
    Document *document = ScriptWrappable::Cast<Document>(thisVal);
    return WrapString(ctx, document->title());
}

static JSValue SetTitle(JSContext *ctx, JSValueConst thisVal, JSValueConst title)
{
    if (!JS_IsString(title))
        return JS_ThrowTypeError(ctx, "Invalid title");

    Document *document = ScriptWrappable::Cast<Document>(thisVal);

    document->setTitle(ToWTFString(ctx, title));
    return WrapString(ctx, document->title());
}

#ifdef BLINKIT_CRAWLER_ENABLED
JSValue CreateDocumentPrototypeForCrawler(JSContext *ctx, JSValue containerNodePrototype)
{
    static const JSCFunctionListEntry Funcs[] = {
        CGetSetEntry(Strings::Body, GetBody, nullptr),
        CFunctionEntry(Strings::CreateComment, 1, CreateComment),
        CFunctionEntry(Strings::CreateDocumentFragment, 0, CreateDocumentFragment),
        CFunctionEntry(Strings::CreateElement, 1, CreateElement),
        CFunctionEntry(Strings::CreateEvent, 1, CreateEvent),
        CFunctionEntry(Strings::CreateTextNode, 1, CreateTextNode),
        CPropStringEntry("designMode", "off"), // Just a placeholder.
        CGetSetEntry(Strings::DocumentElement, GetDocumentElement, nullptr),
        CFunctionEntry(Strings::GetElementById, 1, GetElementById),
        CGetSetEntry(Strings::Title, GetTitle, SetTitle)
    };

    JSValue ret = JS_NewObjectProto(ctx, containerNodePrototype);
    JS_SetPropertyFunctionList(ctx, ret, Funcs, std::size(Funcs));
    return ret;
}
#endif

#ifdef BLINKIT_UI_ENABLED
namespace UI {

static JSValue GetStyleSheets(JSContext *ctx, JSValueConst thisVal)
{
    Document *document = ScriptWrappable::Cast<Document>(thisVal);
    return ScriptController::From(ctx)->Wrap(document->styleSheets());
}

static JSValue ElementFromPoint(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
{
    if (!JS_IsNumber(argv[0]) || !JS_IsNumber(argv[1]))
        return JS_ThrowTypeError(ctx, "Invalid position");

    Document *document = ScriptWrappable::Cast<Document>(thisVal);

    int32_t x, y;
    JS_ToInt32(ctx, &x, argv[0]);
    JS_ToInt32(ctx, &y, argv[1]);

    Element *ret = document->elementFromPoint(x, y);
    return ScriptController::From(ctx)->WrapElement(ret);
}

} // namespace UI

JSValue CreateDocumentPrototypeForUI(JSContext *ctx, JSValue containerNodePrototype)
{
    static const JSCFunctionListEntry Funcs[] = {
        CGetSetEntry(Strings::Body, GetBody, nullptr),
        CFunctionEntry(Strings::CreateComment, 1, CreateComment),
        CFunctionEntry(Strings::CreateDocumentFragment, 0, CreateDocumentFragment),
        CFunctionEntry(Strings::CreateElement, 1, CreateElement),
        CFunctionEntry(Strings::CreateEvent, 1, CreateEvent),
        CFunctionEntry(Strings::CreateTextNode, 1, CreateTextNode),
        CGetSetEntry(Strings::DocumentElement, GetDocumentElement, nullptr),
        CFunctionEntry(Strings::ElementFromPoint, 2, UI::ElementFromPoint),
        CFunctionEntry(Strings::GetElementById, 1, GetElementById),
        CGetSetEntry(Strings::StyleSheets, UI::GetStyleSheets, nullptr),
        CGetSetEntry(Strings::Title, GetTitle, SetTitle)
    };

    JSValue ret = JS_NewObjectProto(ctx, containerNodePrototype);
    JS_SetPropertyFunctionList(ctx, ret, Funcs, std::size(Funcs));
    return ret;
}
#endif

} // namespace qjs
} // namespace BlinKit
